import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;

/**
 * nacitani xml
 * validace xml
 * ukladani xml
 * rozhozeni tagu do treemap/treeset pro gramatiku
 *
 * presunout sem operace s plain textem
 *
 * konverze na dalsi formaty
 *
 * @author Zuzana Krejcova
 */
public abstract class GramatikaIO
{
    private static void save(File soubor, String gramatika) throws Exception
    {
        BufferedWriter vystup = null;
        try {

            vystup = new BufferedWriter(new FileWriter(soubor));
            vystup.write(gramatika);
        } catch (Exception e) {
            throw new Exception(e);
        } finally {
            try {
                vystup.close();
            } catch (Exception e) {
                throw new Exception(e);
            }
        }
    }

    public static void saveAsText(File soubor, Gramatika gramatika) throws Exception
    {
        if (!soubor.getName().endsWith(".txt")) soubor = new File(soubor.getPath()+".txt");
        save(soubor, gramatika.toString());
    }

    public static void saveAsXml(File soubor, Gramatika gramatika) throws Exception
    {
        if (!soubor.getName().endsWith(".xml")) soubor = new File(soubor.getPath()+".xml");
        String gramatikaXml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\n"+
                "<gramatika"+
//                "\nxmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\""+
//                "\nxsi:noNamespaceSchemaLocation='gramatikaXmlSchema.xsd'"+
                ">\n";
        gramatikaXml += "\t<symboly>\n";
        for (String neterm : gramatika.neterminaly)
        {
            gramatikaXml += "\t\t<symbol name=\""+neterm+"\" typ=\"n\" />\n";
        }
        for (String term : gramatika.terminaly)
        {
            gramatikaXml += "\t\t<symbol name=\""+term+"\" typ=\"t\" />\n";
        }
        gramatikaXml += "\t</symboly>\n";
        gramatikaXml += "\t<pravidla>\n";
        for (String neterm : gramatika.pravidla.keySet())
        {
            gramatikaXml += "\t\t<pravidlo>\n\t\t\t<leva_strana name-ref=\""+neterm+"\" />\n\t\t\t<prave_strany>\n";
            for (ArrayList<String> pravidlo : gramatika.pravidla.get(neterm))
            {
                gramatikaXml += "\t\t\t\t<prava_strana>\n";
                for (String symbol : pravidlo)
                {
                    if ((!symbol.equals("[eps]"))&&(!symbol.equals("\u03B5")))
                        gramatikaXml += "\t\t\t\t\t<symbol name-ref=\""+symbol+"\" />\n";
                }
                gramatikaXml += "\t\t\t\t</prava_strana>\n";
            }
            gramatikaXml += "\t\t\t</prave_strany>\n\t\t</pravidlo>\n";
        }
        gramatikaXml += "\t</pravidla>\n";
        gramatikaXml += "\t<koren name-ref=\""+gramatika.koren+"\" />\n";
        gramatikaXml += "</gramatika>";
        save(soubor, gramatikaXml);
    }

    public static void saveAsBison(File soubor, Gramatika gramatika) throws Exception
    {
        if (!soubor.getName().endsWith(".y")) soubor = new File(soubor.getPath()+".y");
        gramatika = underscore(gramatika);
        String gramatikaBison = "%{\n/* Prologue */\n%}\n/* Bison declarations */\n"+
                "%start "+gramatika.koren+"\n%%\n/* Grammar rules */\n";
        for (String key : gramatika.pravidla.keySet())
        {
            gramatikaBison += key+": ";
            int i = 0;
            for (ArrayList<String> pravidlo: gramatika.pravidla.get(key))
            {
                i++;
                for (String symbol : pravidlo)
                {
                    if (gramatika.neterminaly.contains(symbol)) gramatikaBison += symbol+" ";
                    if (gramatika.terminaly.contains(symbol)) gramatikaBison += "\""+symbol+"\" ";
                    if (symbol.equals("[eps]") || symbol.equals("\u03B5")) gramatikaBison += "/* empty */";
                }
                if (gramatika.pravidla.get(key).size() > i) gramatikaBison += "\n"+mezery(key.length())+"| ";
            }
            gramatikaBison += "\n"+mezery(key.length())+";\n";
        }
        gramatikaBison += "%%\n/* Epilogue */";
        save(soubor, gramatikaBison);
    }

    public static void saveAsLatex(File soubor, Gramatika gramatika) throws Exception
    {
        if (!soubor.getName().endsWith(".tex")) soubor = new File(soubor.getPath()+".tex");
        String gramatikaLatex = "\\documentclass[12pt]{article}\n"+
                "\\usepackage[utf8]{inputenc}\n"+
                "\\usepackage[T1]{fontenc}\n"+
                "\\usepackage{cmap}\n"+
                "%\\usepackage[landscape]{geometry} %orientace 'na šířku'\n"+
                "\\begin{document}\n"+
                "\\noindent\\(\n";

        String gramTxt = gramatika.toString();

        gramTxt = gramTxt.replaceAll("-->", "\\\\rightarrow");
        gramTxt = gramTxt.replaceAll("\\n", "\\\\\\\\ \n");
        gramTxt = gramTxt.replaceAll("\\{", "\\\\{");
        gramTxt = gramTxt.replaceAll("\\}", "\\\\}");
        gramTxt = gramTxt.replaceAll("\\u03B5", "\\\\epsilon");

        gramatikaLatex += "G = "+gramTxt;
        gramatikaLatex += "\\)\\end{document}";

        save(soubor, gramatikaLatex);
    }

    public static void saveAsXHtml(File soubor, Gramatika gramatika) throws Exception
    {
        if (!soubor.getName().endsWith(".html")) soubor = new File(soubor.getPath()+".html");
        String gramatikaHtml = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n"+
                "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"+
                "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"+mezery(3)+
                "<head>\n"+mezery(6)+"<meta http-equiv=\"Content-Type\" "+
                "content=\"text/html;charset=utf-8\" />\n<title>Bezkontextová gramatika</title>\n"+
                mezery(3)+"</head>\n"+mezery(3)+"<body>\n\n";

        String gramTxt = mezery(6)+"G = "+gramatika.toString();
        gramTxt = gramTxt.replaceAll("-->", "&rarr;");
        gramTxt = gramTxt.replaceAll("\\u03B5", "&epsilon;");
        gramTxt = gramTxt.replaceAll("\\n", "<br />\n"+mezery(6));

        gramatikaHtml += gramTxt+"\n"+mezery(3)+"</body>\n</html>";

        save(soubor, gramatikaHtml);
    }

    public static Gramatika load(File file) throws FileNotFoundException, IOException, Exception
    {
        if (file.getName().endsWith(".xml")) return loadXml(file);
        if (file.getName().endsWith(".txt")) return loadText(file);

        BufferedReader vstup = new BufferedReader(new FileReader(file));
        if (vstup.readLine().startsWith("<?xml")) return loadXml(file);
        return loadText(file);
    }

    private static Gramatika loadXml(File file) throws ParserConfigurationException, SAXException, IOException, Exception
    {
        DocumentBuilderFactory factoryDom = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = factoryDom.newDocumentBuilder();
        Document gramatikaDoc = builder.parse(file.getPath());
        SchemaFactory factorySch = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        File schemaFile = new File("src/gramatikaXmlSchema.xsd");
        Schema schema = factorySch.newSchema(schemaFile);
        Validator validator = schema.newValidator();
        validator.validate(new DOMSource(gramatikaDoc));
        // pokud se to dostane sem, je xml validni

        TreeSet<String> netermy = new TreeSet();
        TreeSet<String> termy = new TreeSet();
        TreeMap<String,HashSet<ArrayList<String>>> pravidla = new TreeMap();
        String koren;

        NodeList list = gramatikaDoc.getElementsByTagName("symbol");
        for (int i=0; i<list.getLength(); i++)
        {
            if (list.item(i).getParentNode().getNodeName().equals("symboly"))
                if (list.item(i).getAttributes().getNamedItem("typ").getTextContent().equals("n"))
                {
                    netermy.add(list.item(i).getAttributes().getNamedItem("name").getTextContent());
                } else
                {
                    termy.add(list.item(i).getAttributes().getNamedItem("name").getTextContent());
                }
        }
        TreeSet<String> pomSet = new TreeSet();
        pomSet.addAll(netermy);
        pomSet.retainAll(termy);
        if (!pomSet.isEmpty()) throw new Exception("Symbol "+pomSet.first()+" se vyskytuje v terminálech i neterminálech.");
        koren = gramatikaDoc.getElementsByTagName("koren").item(0).getAttributes().getNamedItem("name-ref").getTextContent();
        if (!netermy.contains(koren)) throw new Exception("Kořen gramatiky není mezi neterminály.");
        // nebo pravidla zkusit zevnitr ven? getElementsByTagName("symbol") a hledat rodice?
        list = gramatikaDoc.getElementsByTagName("pravidlo");
        for (int i=0; i<list.getLength(); i++)
        {
            HashSet<ArrayList<String>> pravaStrana = new HashSet();
            String levaStrana = list.item(i).getChildNodes().item(1).getAttributes().getNamedItem("name-ref").getTextContent();
            NodeList pravaStranaList = list.item(i).getChildNodes().item(3).getChildNodes();
            for (int j=0; j<pravaStranaList.getLength(); j++)
            {
                if (pravaStranaList.item(j).getNodeType()==Node.ELEMENT_NODE)
                {
                    ArrayList<String> pravidlo = new ArrayList();
                    NodeList pravidloList = pravaStranaList.item(j).getChildNodes();
                    if (pravidloList.getLength()==1)
                    {
                        pravidlo.add("\u03B5");
                    } else {
                    for (int k=0; k<pravidloList.getLength(); k++)
                    {
                        if (pravidloList.item(k).getNodeType()==Node.ELEMENT_NODE)
                        {
                            String symbol = pravidloList.item(k).getAttributes().getNamedItem("name-ref").getTextContent();
                            if (!netermy.contains(symbol) && ! termy.contains(symbol)) throw new Exception("Symbol "+symbol+" nepatří do gramatiky.");
                            pravidlo.add(symbol);
                        }
                    }}
                    pravaStrana.add(pravidlo);
                }
            }
            pravidla.put(levaStrana, pravaStrana);
        }
        return new Gramatika(netermy, termy, koren, pravidla);
    }

    private static Gramatika loadText(File file) throws Exception
    {
        return new Gramatika(new BufferedReader(new FileReader(file)));
    }

    private static String mezery(int delka)
    {
        String ret = "";
        for (int i=0; i<delka; i++) ret += " ";
        return ret;
    }

    private static Gramatika underscore(Gramatika g)
    {
        TreeSet<String> netermy = new TreeSet();
        TreeMap<String,HashSet<ArrayList<String>>> pravidla = new TreeMap();
        for (String neterm : g.neterminaly)
        {
            if (neterm.matches("<.*>")) {
                netermy.add("_"+neterm.substring(1, neterm.length()-1)+"_");
            } else {
                netermy.add(neterm);
            }
        }
        for (String key : g.pravidla.keySet())
        {
            HashSet<ArrayList<String>> pravaStrana = new HashSet();
            for (ArrayList<String> pravidlo: g.pravidla.get(key))
            {
                ArrayList<String> pravidlo2 = new ArrayList();
                for (String symbol : pravidlo)
                {
                    if (symbol.matches("<.*>")) {
                        pravidlo2.add("_"+symbol.substring(1, symbol.length()-1)+"_");
                    } else {
                        pravidlo2.add(symbol);
                    }
                }
                pravaStrana.add(pravidlo2);
            }
            if (key.matches("<.*>")) {
                pravidla.put("_"+key.substring(1, key.length()-1)+"_", pravaStrana);
            } else {
                pravidla.put(key, pravaStrana);
            }
        }
        return new Gramatika(netermy, g.terminaly, g.koren, pravidla);
    }
}
